/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007-2008 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.projects.toolbox_basics_project.security.manager.impl;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang.RandomStringUtils;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.util.ObjectUtils;

import com.genia.toolbox.basics.exception.technical.TechnicalException;
import com.genia.toolbox.basics.exception.technical.TechnicalIllegalArgumentException;
import com.genia.toolbox.basics.manager.CryptoManager;

/**
 * Implementation of {@link TrampolinePasswordEncoder}.
 */
public class PasswordEncoderImpl
    implements TrampolinePasswordEncoder
{

  /**
   * the length of the generated seeds.
   */
  public static final int SEED_LENGTH = 8;

  /**
   * the CryptoManager to use.
   */
  private CryptoManager cryptoManager;

  /**
   * whether to really encode password or just doing null operation.
   */
  private boolean encodePassword = true;



  /**
   * <p>
   * Encodes the specified raw password with an implementation specific
   * algorithm.
   * </p>
   * <P>
   * This will generally be a one-way message digest such as MD5 or SHA, but may
   * also be a plaintext variant which does no encoding at all, but rather
   * returns the same password it was fed. The latter is useful to plug in when
   * the original password must be stored as-is.
   * </p>
   * <p>
   * The specified salt will potentially be used by the implementation to "salt"
   * the initial value before encoding. A salt is usually a user-specific value
   * which is added to the password before the digest is computed. This means
   * that computation of digests for common dictionary words will be different
   * than those in the backend store, because the dictionary word digests will
   * not reflect the addition of the salt. If a per-user salt is used (rather
   * than a system-wide salt), it also means users with the same password will
   * have different digest encoded passwords in the backend store.
   * </p>
   * <P>
   * If a salt value is provided, the same salt value must be use when calling
   * the {@link #isPasswordValid(String, String, Object)} method. Note that a
   * specific implementation may choose to ignore the salt value (via
   * <code>null</code>), or provide its own.
   * </p>
   * 
   * @param rawPass
   *          the password to encode
   * @param salt
   *          optionally used by the implementation to "salt" the raw password
   *          before encoding. A <code>null</code> value is legal.
   * @return encoded password *
   * @see org.springframework.security.providers.encoding.PasswordEncoder#encodePassword(java.lang.String,
   *      java.lang.Object)
   */
  public String encodePassword(String rawPass, Object salt)
  {
    if (!isEncodePassword()) {
      return rawPass;
    }
    try {
      return realEncodePassword(rawPass, String.valueOf(salt));
    }
    catch (TechnicalException e) {
      throw new DataRetrievalFailureException(e.getMessage(), e);
    }
  }



  /**
   * hash password.
   * @param password
   *          the password.
   * @param seed
   *          if password has been hash.
   * 
   * @return a string representation of the digest.
   * @throws TechnicalException
   *           if the algorithm is not found.
   */
  public String realEncodePassword(final String password, final String seed)
      throws TechnicalException
  {

    if (seed == null) {
      throw new TechnicalIllegalArgumentException("The seed must not be null", seed);
    }
    String realSeed = seed;

    if (seed.contains("$")
    /* && seed.matches("[a-zA-Z0-9]" + "($[a-zA-Z0-9]*)?") */) {
      realSeed = seed.split("\\$")[0];
    }

    if ("".equals(realSeed)) {
      throw new TechnicalIllegalArgumentException("The seed must not be empty", seed);
    }
    return realSeed + "$" + new String(Hex.encodeHex(getCryptoManager().hash(realSeed + password + realSeed)));
  }



  /**
   * <p>
   * Validates a specified "raw" password against an encoded password.
   * </p>
   * <P>
   * The encoded password should have previously been generated by
   * {@link #encodePassword(String, Object)}. This method will encode the
   * <code>rawPass</code> (using the optional <code>salt</code>), and then
   * compared it with the presented <code>encPass</code>.
   * </p>
   * <p>
   * For a discussion of salts, please refer to
   * {@link #encodePassword(String, Object)}.
   * </p>
   * 
   * @param encPass
   *          a pre-encoded password
   * @param rawPass
   *          a raw password to encode and compare against the pre-encoded
   *          password
   * @param salt
   *          optionally used by the implementation to "salt" the raw password
   *          before encoding. A <code>null</code> value is legal.
   * @return true if the password is valid , false otherwise
   * @see org.springframework.security.providers.encoding.PasswordEncoder#isPasswordValid(java.lang.String,
   *      java.lang.String, java.lang.Object)
   */
  public boolean isPasswordValid(String encPass, String rawPass, Object salt)
  {
    return ObjectUtils.nullSafeEquals(encPass, encodePassword(rawPass, salt));
  }



  /**
   * Encodes the specified raw password with an implementation specific
   * algorithm. This method is equivalent to
   * <code>encodePassword(rawPass, generateSeed()).</code>
   * 
   * @param rawPass
   *          the password to encode
   * @return encoded password
   * @see TrampolinePasswordEncoder#encodePassword(String)
   */
  public String encodePassword(String rawPass)
  {
    return encodePassword(rawPass, generateSeed());
  }



  /**
   * getter for the cryptoManager property.
   * 
   * @return the cryptoManager
   */
  public CryptoManager getCryptoManager()
  {
    return cryptoManager;
  }



  /**
   * setter for the cryptoManager property.
   * 
   * @param cryptoManager
   *          the cryptoManager to set
   */
  public void setCryptoManager(CryptoManager cryptoManager)
  {
    this.cryptoManager = cryptoManager;
  }



  /**
   * getter for the encodePassword property.
   * 
   * @return the encodePassword
   */
  public boolean isEncodePassword()
  {
    return encodePassword;
  }



  /**
   * setter for the encodePassword property.
   * 
   * @param encodePassword
   *          the encodePassword to set
   */
  public void setEncodePassword(boolean encodePassword)
  {
    this.encodePassword = encodePassword;
  }



  /**
   * generate a random seed.
   * 
   * @return the seed.
   * @see com.genia.toolbox.projects.toolbox_basics_project.security.manager.impl.TrampolinePasswordEncoder#generateSeed()
   */
  public String generateSeed()
  {
    return RandomStringUtils.randomAlphanumeric(SEED_LENGTH);
  }

}
